<?php

namespace Fluency\Web;

/**
 * A key-value container for various data required in various stages of dispatching a request,
 * e.g. the page title, a reference to the current user identity, or an identifier of the renderer
 * for the requested action. This container can store multiple values with the same key, which is
 * called the type of the value. A type-value pair is called a field.
 *
 * @package Fluency\Web
 * @author  Ignas Rudaitis
 * @since   1.0
 */
class RespondingPolicy
{
    private $fields;

    /**
     * The constructor.
     *
     * @param mixed[] $fields the initial fields (type => value)
     */
    public function __construct(array $fields = array())
    {
        $this->fields = $fields;
    }

    /**
     * Checks if the policy contains any fields of the specified type.
     *
     * @param string $type
     * @return boolean
     */
    public function hasFieldsOfType($type)
    {
        return isset($this->fields[$type]) && !empty($this->fields[$type]);
    }

    /**
     * Gets the value of the first field of the specified type.
     *
     * @param string $type
     * @param mixed $default the value to be returned if no field of the specified type is found
     * @throws OutOfBoundsException if the field is not found and no default value is provided
     * @return mixed
     */
    public function getFirstFieldOfType($type, $default = null)
    {
        return $this->getFieldByTypeAndFunction($type, "reset", $default, func_num_args() == 2);
    }

    /**
     * Gets the value of the last field of the specified type.
     *
     * @param string $type
     * @param mixed $default the value to be returned if no field of the specified type is found
     * @throws OutOfBoundsException if the field is not found and no default value is provided
     * @return mixed
     */
    public function getLastFieldOfType($type, $default = null)
    {
        return $this->getFieldByTypeAndFunction($type, "end", $default, func_num_args() == 2);
    }

    private function getFieldByTypeAndFunction($type, $func, $default, $shouldUseDefault)
    {
        if (isset($this->fields[$type])) {
            return $func($this->fields[$type]);
        } elseif ($shouldUseDefault) {
            return $default;
        } else {
            throw new \OutOfBoundsException("There are no fields of type '$type'");
        }
    }

    /**
     * Gets an array of values of all the fields of the specified type.
     *
     * @param string $type
     * @return mixed[]
     */
    public function getAllFieldsOfType($type)
    {
        return isset($this->fields[$type]) ? $this->fields[$type] : array();
    }

    /**
     * Returns an associative array (indexed by field types) of arrays that contain all the values
     * of fields of each type.
     *
     * @return mixed[][]
     */
    public function getAllFields()
    {
        return $this->fields;
    }

    /**
     * Adds a field of the specified type with the specified value.
     *
     * @param string $type
     * @param mixed $value
     * @return Fluency\Web\RespondingPolicy $this
     */
    public function addField($type, $value = true)
    {
        if (!isset($this->fields[$type])) {
            $this->fields[$type] = array();
        }
        $this->fields[$type][] = $value;
        return $this;
    }

    /**
     * Removes all the fields of the specified type.
     *
     * @param string $type
     * @return Fluency\Web\RespondingPolicy $this
     */
    public function removeAllFieldsOfType($type)
    {
        unset($this->fields[$type]);
        return $this;
    }

    /**
     * Forwards get*(), has*(), add*() and set*() magic method calls.
     * For example:
     * <ul>
     *   <li>getPageTitle([$default]) is forwarded to getLastOfType("PageTitle", [$default])</li>
     *   <li>hasPageTitle() is forwarded to hasFieldsOfType("PageTitle")</li>
     *   <li>addPageTitle([$value]) is forwarded to addField("PageTitle", [$value])</li>
     *   <li>setPageTitle([$value]) means the same as addPageTitle([$value])</li>
     * </ul>
     *
     * @throws LogicException if no forwarding rule is applicable for the specified method name
     * @param string $method
     * @param mixed[] $args
     * @return mixed
     */
    public function __call($method, array $args)
    {
        switch (substr($method, 0, 3)) {
            case "get":
                if (!empty($args)) {
                    return $this->getLastFieldOfType(substr($method, 3), $args[0]);
                }
                return $this->getLastFieldOfType(substr($method, 3));
            case "has":
                return $this->hasFieldsOfType(substr($method, 3));
            case "add":
            case "set":
                return $this->addField(substr($method, 3), isset($args[0]) ? $args[0] : true);
            default:
                throw new \LogicException(
                        "No accessible method named '$method' exists in class " . __CLASS__);
        }
    }
}
